Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / docs / decisions / architecture-review-2026-03.md

Displaying Rendered β€’ View raw β€’ Download

docs/decisions/architecture-review-2026-03.md f07624be882e679affc02931dc75e8cb1594de18 (f07624be) Text, 15.20 KB

Architecture Review β€” March 2026

β”‚ Status: Active Last updated: 2026-03-31

Re-evaluation of project modularity and architecture against modern KMP and Android best practices. Identifies gaps and actionable improvements across modularity, reusability, clean abstractions, DI, and testing.

Executive Summary

The codebase is ~98% structurally KMP β€” 18/20 core modules and 8/8 feature modules declare T383838jvm() targets and cross-compile in CI. Shared T383838commonMain code accounts for ~52K LOC vs ~18K platform-specific LOC (a 74/26 split). This is strong.

Of the five structural gaps originally identified, four are resolved and one remains in progress:

1. T383838app is a God module β€” originally 90 files / ~11K LOC of transport, service, UI, and ViewModel code that should live in core/feature modules. (βœ… Resolved β€” app module reduced to 6 files: T383838MainActivity, T383838MeshUtilApplication, Nav shell, and DI config)
2. ~~Radio transport layer is app-locked~~ β€” βœ… Resolved: T383838RadioTransport interface in T383838core:repository/commonMain; shared T383838StreamFrameCodec + T383838TcpTransport in T383838core:network.
3. ~~T383838java.* APIs leak into T383838commonMain~~ β€” βœ… Resolved: T383838Locale, T383838ConcurrentHashMap, T383838ReentrantLock purged.
4. ~~Zero feature-level T383838commonTest~~ β€” βœ… Resolved: 193 shared tests across all 8 features; T383838core:testing module established.
5. ~~No T383838feature:connections module~~ β€” βœ… Resolved: KMP module with shared UI and dynamic transport detection.

Source Code Distribution

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Source set β”‚ Files β”‚ ~LOC β”‚ Purpose β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838core/*/commonMain β”‚ 337 β”‚ 32,700 β”‚ Shared business/data logic β”‚
β”‚ T383838feature/*/commonMain β”‚ 146 β”‚ 19,700 β”‚ Shared feature UI + ViewModels β”‚
β”‚ T383838feature/*/androidMain β”‚ 62 β”‚ 14,700 β”‚ Platform UI (charts, previews, permissions) β”‚
β”‚ T383838app/src/main β”‚ 6 β”‚ ~300 β”‚ Android app shell (target achieved) β”‚
β”‚ T383838desktop/src β”‚ 26 β”‚ 4,800 β”‚ Desktop app shell β”‚
β”‚ T383838core/*/androidMain β”‚ 49 β”‚ 3,500 β”‚ Platform implementations β”‚
β”‚ T383838core/*/jvmMain β”‚ 11 β”‚ ~500 β”‚ JVM actuals β”‚
β”‚ T383838core/*/jvmAndroidMain β”‚ 4 β”‚ ~200 β”‚ Shared JVM+Android code β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key ratio: 74% of production code is in T383838commonMain (shared). Goal: 85%+.


A. Critical Modularity Gaps

A1. T383838app module is a God module

The T383838app module should be a thin shell (~20 files): T383838MainActivity, DI assembly, nav host. Originally it held 90 files / ~11K LOC, now completely reduced to a 6-file shell:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Area β”‚ Files β”‚ LOC β”‚ Where it should live β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838repository/radio/ β”‚ 22 β”‚ ~2,000 β”‚ T383838core:service / T383838core:network β”‚
β”‚ T383838service/ β”‚ 12 β”‚ ~1,500 β”‚ Extracted to T383838core:service/androidMain βœ“ β”‚
β”‚ T383838navigation/ β”‚ ~1 β”‚ ~200 β”‚ Root Nav 3 host wiring stays in T383838app. Feature… β”‚
β”‚ T383838settings/ ViewModels β”‚ 3 β”‚ ~350 β”‚ Thin Android wrappers (genuine platform deps) β”‚
β”‚ T383838widget/ β”‚ 4 β”‚ ~300 β”‚ Extracted to T383838feature:widget βœ“ β”‚
β”‚ T383838worker/ β”‚ 4 β”‚ ~350 β”‚ Extracted to T383838core:service/androidMain and T383838fe… β”‚
β”‚ DI + Application + MainActivity β”‚ 5 β”‚ ~500 β”‚ Stay in T383838app βœ“ β”‚
β”‚ UI screens + ViewModels β”‚ 5 β”‚ ~1,200 β”‚ Stay in T383838app (Android-specific deps) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Progress: Extracted T383838ChannelViewModel β†’ T383838feature:settings/commonMain, T383838NodeMapViewModel β†’ T383838feature:map/commonMain, T383838NodeContextMenu β†’ T383838feature:node/commonMain, T383838EmptyDetailPlaceholder β†’ T383838core:ui/commonMain. Remaining extractions require radio/service layer refactoring (bigger scope).

A2. Radio interface layer is app-locked and non-KMP

The core transport abstraction was previously locked in T383838app/repository/radio/ via T383838IRadioInterface. This has been successfully refactored:

1. Defined T383838RadioTransport interface in T383838core:repository/commonMain (replacing T383838IRadioInterface)
2. Moved T383838StreamFrameCodec-based framing to T383838core:network/commonMain
3. Moved TCP transport to T383838core:network/jvmAndroidMain
4. The remaining T383838app/repository/radio/ implementations (BLE, Serial, Mock) now implement T383838RadioTransport.

Recommended next steps:
1. Move BLE transport to T383838core:ble/androidMain
2. Move Serial/USB transport to T383838core:service/androidMain

A3. No T383838feature:connections module (resolved 2026-03-12)

Device discovery UI was duplicated:
β€’ Android: T383838app/ui/connections/ (13 files: T383838ConnectionsScreen, T383838ScannerViewModel, 10 components)
β€’ Desktop: T383838desktop/ui/connections/DesktopConnectionsScreen.kt (separate implementation)

Outcome: Created T383838feature:connections KMP module with:
β€’ T383838commonMain: T383838ScannerViewModel, T383838ConnectionsScreen, 11 shared UI components, T383838DeviceListEntry sealed class, T383838GetDiscoveredDevicesUseCase interface, T383838CommonGetDiscoveredDevicesUseCase (TCP/recent devices)
β€’ T383838androidMain: T383838AndroidScannerViewModel (BLE bonding, USB permissions), T383838AndroidGetDiscoveredDevicesUseCase (BLE/NSD/USB discovery), T383838NetworkRepository, T383838UsbRepository, T383838SerialConnection
β€’ Desktop uses the shared T383838ConnectionsScreen + T383838CommonGetDiscoveredDevicesUseCase directly
β€’ Dynamic transport detection via T383838RadioInterfaceService.supportedDeviceTypes
β€’ Module registered in both T383838AppKoinModule and T383838DesktopKoinModule

A4. T383838core:api AIDL coupling

T383838core:api is Android-only (AIDL IPC). T383838ServiceClient in T383838core:service/androidMain wraps it. Desktop doesn't use it β€” it has T383838DirectRadioControllerImpl in T383838core:service/commonMain.

Recommendation: The T383838DirectRadioControllerImpl pattern is correct. Ensure T383838RadioController (already in T383838core:model/commonMain) is the canonical interface; deprecate the AIDL-based path for in-process usage.


B. KMP Platform Purity

B1. T383838java.util.Locale leaks in T383838commonMain (resolved 2026-03-11)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ File β”‚ Usage β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838core:data/.../TracerouteHandlerImpl.kt β”‚ Replaced with T383838NumberFormatter.format(seconds, 1) β”‚
β”‚ T383838core:data/.../NeighborInfoHandlerImpl.kt β”‚ Replaced with T383838NumberFormatter.format(seconds, 1) β”‚
β”‚ T383838core:prefs/.../MeshPrefsImpl.kt β”‚ Replaced with locale-free T383838uppercase() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Outcome: The three T383838Locale usages identified in March were removed from T383838commonMain. Follow-up cleanup in the same sprint also moved T383838ReentrantLock-based T383838SyncContinuation to T383838jvmAndroidMain, replaced prefs T383838ConcurrentHashMap caches with atomic persistent maps, and pushed enum reflection behind T383838expect/T383838actual so no known T383838java.* runtime calls remain in T383838commonMain.

B2. T383838ConcurrentHashMap leaks in T383838commonMain (resolved 2026-03-11)

Formerly found in 3 prefs files:
β€’ T383838core:prefs/.../MeshPrefsImpl.kt
β€’ T383838core:prefs/.../UiPrefsImpl.kt
β€’ T383838core:prefs/.../MapConsentPrefsImpl.kt

Outcome: These caches now use T383838AtomicRef<PersistentMap<...>> helpers in T383838commonMain, eliminating the last T383838ConcurrentHashMap usage from shared prefs code.

B3. MQTT (Resolved)

T383838MQTTRepositoryImpl has been migrated to T383838commonMain using KMQTT, replacing Eclipse Paho.

Fix: Completed.
β€’ T383838kmqtt library integrated for full KMP support.

B4. Vico charts (resolved)

Vico chart screens (DeviceMetrics, EnvironmentMetrics, SignalMetrics, PowerMetrics, PaxMetrics) have been migrated to T383838feature:node/commonMain using Vico's KMP artifacts (T383838vico-compose, T383838vico-compose-m3). Desktop wires them via shared composables. No Android-only chart code remains.

B5. Cross-platform code deduplication (resolved 2026-03-21)

Comprehensive audit of T383838androidMain vs T383838jvmMain duplication across all feature modules. Extracted shared components:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Component β”‚ Module β”‚ Eliminated fr… β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838AlertHost composable β”‚ T383838core:ui/commonMain β”‚ Android T383838Main.… β”‚
β”‚ T383838SharedDialogs composable β”‚ T383838core:ui/commonMain β”‚ Android T383838Main.… β”‚
β”‚ T383838PlaceholderScreen composable β”‚ T383838core:ui/commonMain β”‚ 4 copies: T383838des… β”‚
β”‚ T383838ThemePickerDialog + T383838ThemeOption β”‚ T383838feature:settings/commonMain β”‚ Android T383838Setti… β”‚
β”‚ T383838formatLogsTo() + T383838redactedKeys β”‚ T383838feature:settings/commonMain (T383838LogFormatter.kt) β”‚ Android + Des… β”‚
β”‚ T383838handleNodeAction() β”‚ T383838feature:node/commonMain β”‚ Android T383838NodeD… β”‚
β”‚ T383838findNodeByNameSuffix() β”‚ T383838feature:connections/commonMain β”‚ Android USB m… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Also fixed T383838Dispatchers.IO usage in T383838StoreForwardPacketHandlerImpl (would break iOS), removed dead T383838UIViewModel.currentAlert property, and added T383838firebase-debug.log to T383838.gitignore.


C. DI Improvements

C1. ~~Desktop manual ViewModel wiring~~ (resolved 2026-03-13)

T383838DesktopKoinModule.kt originally had ~120 lines of hand-written T383838viewModel { ... } blocks. These have been successfully replaced by including Koin modules from T383838commonMain generated via the Koin K2 Compiler Plugin for automatic wiring.

C2. ~~Desktop stubs lack compile-time validation~~ (resolved 2026-03-13)

T383838desktopPlatformStubsModule() previously had stubs that were only validated at runtime.

Outcome: Added T383838DesktopKoinTest.kt using Koin's T383838verify() API. This test validates the entire Desktop DI graph (including platform stubs and DataStores) during the build. Discovered and fixed missing stubs for T383838CompassHeadingProvider, T383838PhoneLocationProvider, and T383838MagneticFieldProvider.

C3. DI module naming convention

Android uses T383838@Module-annotated classes (T383838CoreDataModule, T383838CoreBleAndroidModule). Desktop imports them as T383838CoreDataModule().coreDataModule(). This works but the double-invocation pattern is non-obvious.

Recommendation: Document the pattern in AGENTS.md. Consider if Koin Annotations 2.x supports a simpler import syntax.


D. Test Architecture

D1. Zero T383838commonTest in feature modules (resolved 2026-03-12)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Module β”‚ `BT383838`FdddcommonTest`f`b β”‚ `BT383838`Fdddtest`f`b/`BT383838`FdddandroidUnitTest`f`b β”‚ `BT383838`FdddandroidTest`f`b β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838feature:settings β”‚ 22 β”‚ 20 β”‚ 15 β”‚
β”‚ T383838feature:node β”‚ 24 β”‚ 9 β”‚ 0 β”‚
β”‚ T383838feature:messaging β”‚ 18 β”‚ 5 β”‚ 3 β”‚
β”‚ T383838feature:connections β”‚ 27 β”‚ 0 β”‚ 0 β”‚
β”‚ T383838feature:firmware β”‚ 15 β”‚ 25 β”‚ 0 β”‚
β”‚ T383838feature:wifi-provision β”‚ 62 β”‚ 0 β”‚ 0 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Outcome: All 8 feature modules now have T383838commonTest coverage (193 shared tests). Combined with 70 platform unit tests and 18 instrumented tests, feature modules have 281 tests total.

D2. No shared test fixtures (resolved 2026-03-12)

T383838core:testing module established with shared fakes (T383838FakeNodeRepository, T383838FakeServiceRepository, T383838FakeRadioController, T383838FakePacketRepository) and T383838TestDataFactory builders. Used by all feature T383838commonTest suites.

D3. Core module test gaps

36 T383838commonTest files exist but are concentrated in T383838core:domain (22 files) and T383838core:data (10 files). Limited or zero tests in:
β€’ T383838core:service (has T383838ServiceRepositoryImpl, T383838DirectRadioControllerImpl, T383838MeshServiceOrchestrator)
β€’ T383838core:network (has T383838StreamFrameCodecTest β€” 10 tests; T383838TcpTransport untested)
β€’ T383838core:prefs (preference flows, default values)
β€’ T383838core:ble (connection state machine)
β€’ T383838core:ui (utility functions)

D4. Desktop has 2 tests

T383838desktop/src/test/ contains T383838DesktopKoinTest.kt and T383838DesktopTopLevelDestinationParityTest.kt. Still needs:
β€’ Navigation graph coverage


E. Module Extraction Priority

Ordered by impact Γ— effort:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Priority β”‚ Extraction β”‚ Impact β”‚ Effort β”‚ Enables β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1 β”‚ ~~T383838java.* purge from T383838commonMai… β”‚ High β”‚ Low β”‚ ~~iOS target declaration~~ βœ… Done β”‚
β”‚ 2 β”‚ Radio transport interfaces to… β”‚ High β”‚ Medium β”‚ Transport unification β”‚
β”‚ 3 β”‚ T383838core:testing shared fixtures … β”‚ Medium β”‚ Low β”‚ Feature commonTest β”‚
β”‚ 4 β”‚ Feature T383838commonTest (D1) β”‚ Medium β”‚ Medium β”‚ KMP test coverage β”‚
β”‚ 5 β”‚ T383838feature:connections (A3) β”‚ High β”‚ Medium β”‚ ~~Desktop connections~~ βœ… Done β”‚
β”‚ 6 β”‚ Service/worker extraction fro… β”‚ Medium β”‚ Medium β”‚ Thin app module β”‚
β”‚ 7 β”‚ ~~Desktop Koin auto-wiring (C… β”‚ Medium β”‚ Low β”‚ βœ… Resolved 2026-03-13 β”‚
β”‚ 8 β”‚ MQTT KMP (B3) β”‚ Medium β”‚ High β”‚ Desktop/iOS MQTT β”‚
β”‚ 9 β”‚ KMP charts (B4) β”‚ Medium β”‚ High β”‚ Desktop metrics β”‚
β”‚ 10 β”‚ ~~iOS target declaration~~ β”‚ High β”‚ Low β”‚ ~~CI purity gate~~ βœ… Done β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


Scorecard Update

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Area β”‚ Previous β”‚ Current β”‚ Notes β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Shared business/data logic β”‚ 8.5/10 β”‚ 9/10 β”‚ RadioTransport interface unified; all core la… β”‚
β”‚ Shared feature/UI logic β”‚ 9.5/10 β”‚ 9/10 β”‚ All 8 KMP features; connections unified; cros… β”‚
β”‚ Android decoupling β”‚ 8.5/10 β”‚ 9/10 β”‚ Connections, Navigation, Services, & Widgets … β”‚
β”‚ Multi-target readiness β”‚ 8/10 β”‚ 9/10 β”‚ Full JVM; release-ready desktop; iOS simulato… β”‚
β”‚ CI confidence β”‚ 8.5/10 β”‚ 9/10 β”‚ 26 modules validated; feature:connections + f… β”‚
β”‚ DI portability β”‚ 7/10 β”‚ 8/10 β”‚ Koin annotations in commonMain; supportedDevi… β”‚
β”‚ Test maturity β”‚ β€” β”‚ 9/10 β”‚ Mokkery, Turbine, and Kotest integrated; prop… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


F. JVM/Desktop Database Lifecycle

Room KMP's T383838setAutoCloseTimeout API is Android-only. On JVM/Desktop, once a Room database is built, its SQLite connections (5 per WAL-mode DB: 4 readers + 1 writer) remain open indefinitely until explicitly closed via T383838RoomDatabase.close().

Problem

When a user switches between multiple mesh devices, the previous device's database remained open in the in-memory cache. Each idle database consumed ~32 MB (connection pool + prepared statement caches), leading to unbounded memory growth proportional to the number of devices ever connected in a session.

Solution

T383838DatabaseManager.switchActiveDatabase() now explicitly closes the previously active database via T383838closeCachedDatabase() before activating the new one. The closed database is removed from the in-memory cache but its file is preserved, allowing transparent re-opening on next access.

Additional fixes applied:
1. Init-order bug: T383838dbCache was declared after T383838currentDb, causing NPE during T383838stateIn's T383838initialValue evaluation. Reordered to ensure T383838dbCache is initialized first.
2. Corruption handlers: T383838ReplaceFileCorruptionHandler added to T383838createDatabaseDataStore() on both JVM and Android, preventing DataStore corruption from crashing the app.
3. T383838desktopDataDir() deduplication: Made public in T383838core:database/jvmMain and removed the duplicate from T383838DesktopPlatformModule, establishing a single source of truth for the desktop data directory.
4. DataStore scope consolidation: Replaced two separate T383838CoroutineScope instances with a single shared T383838dataStoreScope in T383838DesktopPlatformModule.
5. Coil cache path: Desktop T383838Main.kt updated to use T383838desktopDataDir() instead of hardcoded T383838user.home.


References

β€’ Current migration status: kmp-status.md
β€’ Roadmap: roadmap.md
β€’ Agent guide: ../AGENTS.md
β€’ Decision records: decisions/

Served by rngit 1.5.2 - Generated in 0.13s